/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network;

import java.nio.ByteBuffer;

/**
 * A {@code NetworkPacket} instance contains data which is sent over the network. A packet consists of two parts,
 * a header and content. The header contains information such as the packet's sequence number and an acknowledgement
 * bitfield. The content contains the application-specific data.
 */
public class NetworkPacket {
	/**
	 * Sequence numbers must be at least this value.
	 */
	public static final int MIN_SEQUENCE_NUMBER_VALUE   = 0;

	/**
	 * Sequence numbers must be at most this value.
	 */
	public static final int MAX_SEQUENCE_NUMBER_VALUE   = 65535;

	/**
	 * Constructs the packet using the settings provided.
	 *
	 * @param   settings provides settings for the packet, including the maximum packet size.
	 */
	public NetworkPacket(NetworkSettings settings) {
		if (settings.maxPacketSize < HEADER_SIZE_BYTES) {
			throw new IllegalArgumentException("Maximum packet size too small");
		}

		packetDataSize = HEADER_SIZE_BYTES;
		packetData = new byte[settings.maxPacketSize];
	}

	/**
	 * Returns a reference to the underlying data buffer for the packet. Note that this reference is mutable:
	 * modifying it will modifying the packet's data.
	 *
	 * @return  the packet's data buffer.
	 */
	public byte[] getPacketData() {
		return packetData;
	}

	/**
	 * Returns the size of the packet's data. This value is equal to the sum of the header size and content size.
	 *
	 * @return  size of the packet's data.
	 */
	public int getPacketDataSize() {
		return packetDataSize;
	}

	/**
	 * Returns whether the packet data size provided is valid. This method should be called before actually assigning
	 * data to the packet. If {@code size} is less than the packet's header size, false is returned.
	 *
	 * @param   size the data size to check.
	 * @return  whether the given data size is valid.
	 */
	public boolean isPacketDataSizeValid(int size) {
		return size >= HEADER_SIZE_BYTES;
	}

	/**
	 * Returns the offset of the packet header in the packet data buffer.
	 *
	 * @return the offset of the packet header.
	 */
	public int getPacketHeaderOffset() {
		return 0;
	}

	/**
	 * Returns the size of the packet header in the packet data buffer.
	 *
	 * @return the size of the packet header.
	 */
	public int getPacketHeaderSize() {
		return HEADER_SIZE_BYTES;
	}

	/**
	 * Returns the offset of the packet content in the packet data buffer.
	 *
	 * @return the offset of the packet content.
	 */
	public int getPacketContentOffset() {
		return HEADER_SIZE_BYTES;
	}

	/**
	 * Sets the size of the packet content in the packet data buffer.
	 *
	 * @param   size the size to assign.
	 * @throws  IllegalArgumentException if {@code size} is negative or exceeds the maximum allowed content size.
	 */
	public void setPacketContentSize(int size) {
		if (size < 0 || size > getMaxPacketContentSize()) {
			throw new IllegalArgumentException("Invalid packet content size");
		}

		packetDataSize = HEADER_SIZE_BYTES + size;
	}

	/**
	 * Returns the size of the packet content in the packet data buffer.
	 *
	 * @return the size of the packet content.
	 */
	public int getPacketContentSize() {
		return packetDataSize - HEADER_SIZE_BYTES;
	}

	/**
	 * Returns the maximum allowed packet content size.
	 *
	 * @return  the maximum allowed packet content size.
	 */
	public int getMaxPacketContentSize() {
		return packetData.length - HEADER_SIZE_BYTES;
	}

	/**
	 * Sets the protocol ID in the packet's header.
	 *
	 * @param   protocolId the protocol ID.
	 */
	public void setProtocolId(int protocolId) {
		// write with an offset of 0
		ByteBuffer buf = ByteBuffer.wrap(packetData, 0, 4);
		buf.putInt(protocolId);
	}

	/**
	 * Returns the protocol ID in the packet's header.
	 *
	 * @return  the protocol ID.
	 */
	public int getProtocolId() {
		// read with an offset of 0
		ByteBuffer buf = ByteBuffer.wrap(packetData, 0, 4);
		return buf.getInt();
	}

	/**
	 * Sets the sequence number in the packet's header. The sequence number must be in the range 0-65535.
	 *
	 * @param   sequenceNumber the sequence number to set.
	 * @throws  IllegalArgumentException if the sequence number is out of range.
	 */
	public void setSequenceNumber(int sequenceNumber) {
		if (sequenceNumber < MIN_SEQUENCE_NUMBER_VALUE || sequenceNumber > MAX_SEQUENCE_NUMBER_VALUE) {
			throw new IllegalArgumentException("Sequence number out of range");
		}

		short shortSeqNum = (short) (sequenceNumber & 0xffff);
		ByteBuffer buf = ByteBuffer.wrap(packetData, 4, 2);
		buf.putShort(shortSeqNum);
	}

	/**
	 * Returns the sequence number in the packet's header.
	 *
	 * @return  the sequence number.
	 */
	public int getSequenceNumber() {
		ByteBuffer buf = ByteBuffer.wrap(packetData, 4, 2);
		return (int) buf.getShort();
	}

	/**
	 * Sets the acknowledgement sequence number and bitfield in the packet's header. The acknowledgement sequence
	 * number must be in the range 0-65535.
	 *
	 * @param   ackSequenceNumber the acknowledgement sequence number.
	 * @param   ackBitfield the acknowledgement bitfield.
	 */
	public void setAck(int ackSequenceNumber, int ackBitfield) {
		if (ackSequenceNumber < MIN_SEQUENCE_NUMBER_VALUE || ackSequenceNumber > MAX_SEQUENCE_NUMBER_VALUE) {
			throw new IllegalArgumentException("Acknowledgement sequence number out of range");
		}

		short shortAckSeqNum = (short) (ackSequenceNumber & 0xffff);
		ByteBuffer buf = ByteBuffer.wrap(packetData, 6, 6);
		buf.putShort(shortAckSeqNum);
		buf.putInt(ackBitfield);
	}

	/**
	 * Returns the acknowledgement sequence number in the packet's header.
	 *
	 * @return  the acknowledgement sequence number.
	 */
	public int getAckSequenceNumber() {
		ByteBuffer buf = ByteBuffer.wrap(packetData, 6, 2);
		return (int) buf.getShort();
	}

	/**
	 * Returns the acknowledgement bitfield in the packet's header.
	 *
	 * @return  the acknowledgement bitfield.
	 */
	public int getAckBitfield() {
		ByteBuffer buf = ByteBuffer.wrap(packetData, 8, 4);
		return buf.getInt();
	}

	// the length of the header in bytes
	private static final int HEADER_SIZE_BYTES  = 12;
	// header layout:
	// type name            size    offset
	// uint protocolId      4       0
	// uint sequenceNumber  2       4
	// uint ack             2       6
	// uint ackBitfield     4       8

	private int packetDataSize; // total size of the packet data
	private byte[] packetData;  // packet data buffer for both header and content
}
